﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Noris.Schedule.UI.Components
{
    /// <summary>
    /// Třída, kterou lze používat pro řízení setrvačnosti jevů (typicky setrvačnost pohybu časové osy, když ji rozhýbám myší a pak myš pustím, a osa pak setrvačností dobíhá)
    /// </summary>
    public class Inertial<T> : IDisposable
    {
        #region KONSTRUKCE, PROMĚNNÉ
        /// <summary>
        /// Konstruktor setrvačnosti.
        /// Předává se parametr, který specifikuje dobu [v sekundách], po kterou se měří rychlost pohybu před jeho uvolněním.
        /// Typická hodnota je okolo 0.2 sekund.
        /// Předává se počet vzorků za sekundu, které se mají sbírat při trasování a které se mají generovat posléze za setrvačného pohybu.
        /// </summary>
        /// <param name="measureInterval">Doba [v sekundách], po kterou se měří rychlost pohybu před jeho uvolněním. Typická hodnota je okolo 0.2 sekund.</param>
        /// <param name="samplePerSecond">Počet vzorků za sekundu, v rozmezí 1 až 250. Záleží na požadované přesnosti a očekávané rychlosti aplikace (složitosti dat).</param>
        public Inertial(int samplePerSecond, double measureInterval, object userData)
        {
            this._MeasureInterval = measureInterval;
            this._SampleInterval = (samplePerSecond <= 0 ? 1000 : samplePerSecond > 250 ? 250 : (int)(1000M / (decimal)samplePerSecond));
            this.UserData = userData;
            this._MeasureCount = (int)(measureInterval * 200D);          // Počet záznamů, které měřím
            this._CurrentMode = _MoveMode.None;
            this._Timer = new System.Threading.Timer(this._TimerTick, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
        }
        private double _MeasureInterval;
        /// <summary></summary>
        private int _SampleInterval;
        protected object UserData;
        private int _MeasureCount;
        private int _SampleIndex;
        private DateTime _Begin;
        private _MoveMode _CurrentMode;
        private System.Collections.Generic.Queue<T> _Queue;
        private System.Threading.Timer _Timer;
        private InertialValueGetHandler _GetValueHandler;
        private InertialRunDownHandler _RunDownHandler;
        private InertialCalculateEnergyEventArgs _CalculateArgs;
        private double _EnergyIntegral;
        private double _EnergyCurrent;
        private double _EnergyMovementRatio;
        private T _LastValue;
        #endregion
        #region PUBLIC METODY
        /// <summary>
        /// Metoda, která zahajuje "trasování" pohybu.
        /// Metoda ihned po zavolání vrací řízení.
        /// Následně v pravidelných intervalech (daných počtem vzorků za sekundu, viz konstruktor) volá handler getValueHandler, a zjišťuje aktuální polohu a ukládá si ji.
        /// Trasování je třeba ukončit zavoláním metody MoveStop().
        /// </summary>
        /// <param name="getValueHandler">Odkaz na metodu, která bude volána v pravidelném intervalu a bude zjišťovat polohu při trasování pohybu.</param>
        public virtual void BeginMove(InertialValueGetHandler getValueHandler)
        {
            this.Stop();

            this._Begin = DateTime.Now;
            this._SampleIndex = 0;
            this._CurrentMode = _MoveMode.Move;
            this._Queue = new Queue<T>();
            this._GetValueHandler = getValueHandler;
            if (getValueHandler != null)
                this._TimerStart();
            
        }
        /// <summary>
        /// Metoda, která ukončuje trasování aktivního pohybu, a zahajuje "letovou" fázi (kdy se pohyb provádí setrvačností).
        /// Je třeba předat odkaz na metody, kde první metoda vypočte parametry pohybu, a druhá bude následně dostávat "vypočtené souřadnice letu".
        /// </summary>
        /// <param name="calcHandler"></param>
        /// <param name="runDownHandler"></param>
        public virtual void BeginFly(InertialCalculateEnergyHandler calcHandler, InertialRunDownHandler runDownHandler)
        {
            _MoveMode mode = this._CurrentMode;
            this._CurrentMode = _MoveMode.None;
            if (mode == _MoveMode.Move && calcHandler != null && runDownHandler != null)
            {
                InertialCalculateEnergyEventArgs e = new InertialCalculateEnergyEventArgs(this._SampleIndex, (DateTime.Now - this._Begin), this.UserData, new List<T>(this._Queue));
                calcHandler(this, e);

                if (e.StartFlyEnabled)
                {
                    this._EnergyIntegral = 0D;
                    this._EnergyCurrent = e.InitialKineticEnergy;
                    this._EnergyMovementRatio = e.EnergyMovementRatio;
                    this._RunDownHandler = runDownHandler;
                    this._CalculateArgs = e;
                    this._CurrentMode = _MoveMode.Fly;
                    this._TimerStart();
                }
            }
        }
        /// <summary>
        /// Zastaví pohyb - v jakékoli situaci
        /// </summary>
        public virtual void Stop()
        {
            this._CurrentMode = _MoveMode.None;
            this._TimerStop();
            this._GetValueHandler = null;
            this._RunDownHandler = null;
            this._CalculateArgs = null;

            this.UserData = null;
            this._LastValue = default(T);
            this._EnergyIntegral  = 0D;
            this._EnergyCurrent = 0D;
            this._EnergyMovementRatio = 0D;
        }
        #endregion
        #region FYZICKÁ PRÁCE S TIMEREM
        private void _TimerStart()
        {
            this._Timer.Change(0, this._SampleInterval);
        }
        private void _TimerStop()
        {
            this._Timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
        }
        private void _TimerTick(object state)
        {
            switch (this._CurrentMode)
            {
                case _MoveMode.Move:
                    this._TickMove();
                    break;
                case _MoveMode.Fly:
                    this._TickFly();
                    break;
            }
        }
        #endregion
        #region TRASOVÁNÍ POHYBU
        private void _TickMove()
        {
            if (this._Queue.Count >= this._MeasureCount)
                this._Queue.Dequeue();
            InertialValueGetEventArgs e = new InertialValueGetEventArgs(this._SampleIndex++, (DateTime.Now - this._Begin), this.UserData);
            this._GetValueHandler(this, e);
            this._Queue.Enqueue(e.Value);
            this.UserData = e.UserData;
        }
        #endregion
        #region SETRVAČNÝ POHYB
        private void _TickFly()
        {
            this._CurrentMode = _MoveMode.None;

            // Výpočet energie = rychlost, koeficient, celková trasa:
            // Integrujeme (prosým sčítáním) celkovou energii += aktuální energie:
            this._EnergyIntegral += this._EnergyCurrent;

            // Vypočteme energii na příští cyklus:
            double energyMovementRatio = this._EnergyMovementRatio;
            energyMovementRatio = (energyMovementRatio < 0D ? 0D : (energyMovementRatio >= 0.99995D ? 0.99995D : energyMovementRatio));         // Hodnota 1.000 už je nepřípustná, musí být méně než 1 (0.99995)

            // Přepočet koeficientu, který je definuje změnu za jednu sekundu tak, abychom z něj mohli určit změnu na jeden vzorek, jehož čas je (this._SampleInterval) [milisec]:
            // Jak určit koeficient na jeden vzorek? Snadno: jde o zlomkovou mocninu základu energyMovementRatio na exponent = (1/počet vzorků za sekundu).
            // Exponent si zjednodušíme, protože máme (this._SampleInterval) interval mezi vzorky v milisekundách:
            double exponent = (double)this._SampleInterval / 1000D;
            double sampleRatio = Math.Exp(Math.Log(energyMovementRatio) * exponent);

            this._EnergyCurrent = this._EnergyCurrent * sampleRatio;

            // Volání cíle:
            InertialRunDownEventArgs e = new InertialRunDownEventArgs(this._SampleIndex++, (DateTime.Now - this._Begin), this.UserData, this._CalculateArgs, energyMovementRatio, this._LastValue, this._EnergyCurrent, this._EnergyIntegral);
            this._RunDownHandler(this, e);

            // Přepis hodnot zpátky:
            this.UserData = e.UserData;
            this._LastValue = e.CurrentValue;
            this._EnergyMovementRatio = e.EnergyMovementRatio;

            // Zastavení?
            if (e.Finished)
                this.Stop();
            else
                this._CurrentMode = _MoveMode.Fly;
        }
        #endregion
        #region DELEGÁTI A EVENTARGS (generické), ENUM
        /// <summary>
        /// Delegát na metodu, která získá hodnotu (= meření v době aktivního pohybu).
        /// Hodnotu uloží do e.Value (generikum)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public delegate void InertialValueGetHandler(object sender, InertialValueGetEventArgs e);
        /// <summary>
        /// Delegát na metodu, která vypočítá výchozí energii pohybu v okamžiku konce jeho aktivní části (tj. "rychlost, na kterou je pohyb rozjetý"), z pole získaných vzorků.
        /// Výchozí energii (jakýkoli double koeficient) uloží do e.InitialKineticEnergy
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public delegate void InertialCalculateEnergyHandler(object sender, InertialCalculateEnergyEventArgs e);
        /// <summary>
        /// Delegát na metodu, která realizuje setrvačný pohyb po skončení aktivního pohybu.
        /// Metoda dostává přehled posledních aktivních vzorků, vypočtenou výchozí energii a postupně se měnící koeficient cílové hodnoty, která je integrálem postupně se zpomalujícího pohybu.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public delegate void InertialRunDownHandler(object sender, InertialRunDownEventArgs e);
        public class InertialEventArgs : EventArgs
        {
            public InertialEventArgs(int sampleIndex, TimeSpan timeFromBegin, object userData)
            {
                this.SampleIndex = sampleIndex;
                this.TimeFromBegin = timeFromBegin;
                this.UserData = userData;
            }
            /// <summary>
            /// Číslo vzorku od počátku měření
            /// </summary>
            public int SampleIndex { get; private set; }
            /// <summary>
            /// Čas od počátku měření
            /// </summary>
            public TimeSpan TimeFromBegin { get; private set; }
            /// <summary>
            /// Uživatelská data. Tento objekt se předává mezi všemi metodami.
            /// </summary>
            public object UserData { get; set; }
        }
        /// <summary>
        /// Data pro načtení aktuální pozice
        /// </summary>
        public class InertialValueGetEventArgs : InertialEventArgs
        {
            public InertialValueGetEventArgs(int sampleIndex, TimeSpan timeFromBegin, object userData)
                : base(sampleIndex, timeFromBegin, userData)
            { }
            /// <summary>
            /// Hodnota, která je platná v tomto čase.
            /// Metoda ji musí načíst a vyplnit.
            /// </summary>
            public T Value { get; set; }
        }
        /// <summary>
        /// Data pro výpočet výchozí energie, při přechodu z aktivního pohybu do pohybu setrvačného.
        /// </summary>
        public class InertialCalculateEnergyEventArgs : InertialEventArgs
        {
            public InertialCalculateEnergyEventArgs(int sampleIndex, TimeSpan timeFromBegin, object userData, List<T> valueList)
                : base(sampleIndex, timeFromBegin, userData)
            {
                this.ValueList = valueList;
            }
            /// <summary>
            /// Přehled hodnot, naměřených za poslední čas aktivního pohybu.
            /// </summary>
            public List<T> ValueList { get; private set; }
            /// <summary>
            /// Sem metoda vepíše true, pokud vstupní data umožňují nastartovat setrvačný pohyb.
            /// Pokud například je málo vzorků (aktivní pohyb trval příliš krátce), anebo mezi vzorky je málo pohybu, pak se setrvačnost nenastartuje.
            /// </summary>
            public bool RunInertialMove { get; set; }
            /// <summary>
            /// Sem metoda vepíše hodnotu (generická), která je výchozím bodem setrvačného pohybu. Typicky jde o poslední hodnotu v seznamu ValueList.
            /// </summary>
            public T InitialValue { get; set; }
            /// <summary>
            /// Sem metoda vepíše údaj o výchozí "energii pohybu", která odpovídá rychlosti pohybu mezi dvěma vzorky.
            /// Tuto energii bude algoritmus "setrvačnosti" brát jako výchozí "rychlost", z níž bude počítat "dráhu setrvačnosti v čase".
            /// </summary>
            public double InitialKineticEnergy { get; set; }
            /// <summary>
            /// Sem metoda vepíše údaj o požadované setrvačnosti:
            /// Pokud bude nastavena hodnota 0, pak pohyb nemá setrvačnost (nikdy se nebude pohybovat setrvačností)
            /// Pokud bude nastavena hodnota 1, pak má pohyb absolutní setrvačnost (po rozpohybování se nikdy sám od sebe nezastaví).
            /// Setrvačnost vyjadřuje relativní změnu rychlosti pohybu za jednu sekundu.
            /// Příklad: při výchozí rychlosti 250 a EnergyMovementRatio = 0.80D bude po 1 sekundě rychlost = (0.80 * 250) = 200, 
            ///   ve druhé sekundě pak (0.80 * 200) = 160, ve třetí sekundě (0.80 * 160) = 128, atd.
            /// </summary>
            public double EnergyMovementRatio { get; set; }
            /// <summary>
            /// Sem metoda vepíše hodnotu, která představuje nejmenší velikost pohybu mezi dvěma po sobě jdoucími časy.
            /// Jakmile se vyhodnotí pohyb menší než tento, pak pohyb skončí.
            /// Může zde zůstat 0, pak pohyb skončí až nastavením Finished = true v metodě pro obsluhu pohybu (delegát InertialRunDownHandler, třída argumentu InertialRunDownEventArgs).
            /// </summary>
            public double TerminalChangeRatio { get; set; }
            /// <summary>
            /// Obsahuje true, pokud data v argumentu umožňují nastartovat pohyb.
            /// Musí být RunInertialMove == true a InitialKineticEnergy různé od 0 a EnergyDeclineRatio větší než 0
            /// </summary>
            public bool StartFlyEnabled
            {
                get
                {
                    return (this.RunInertialMove && this.InitialKineticEnergy != 0D && this.EnergyMovementRatio > 0D);
                }
            }
        }
        /// <summary>
        /// Data pro promítnutí setrvačného pohybu
        /// </summary>
        public class InertialRunDownEventArgs : InertialEventArgs
        {
            public InertialRunDownEventArgs(int sampleIndex, TimeSpan timeFromBegin, object userData, InertialCalculateEnergyEventArgs calcArgs, double energyMovementRatio, T lastValue, double currentEnergyRatio, double currentValueRatio)
                : base(sampleIndex, timeFromBegin, userData)
            {
                this.ValueList = calcArgs.ValueList;
                this.InitialValue = calcArgs.InitialValue;
                this.LastValue = lastValue;
                this.InitialKineticEnergy = calcArgs.InitialKineticEnergy;
                this.EnergyMovementRatio = energyMovementRatio;
                this.CurrentEnergyRatio = currentEnergyRatio;
                this.CurrentValueRatio = currentValueRatio;
            }
            /// <summary>
            /// Přehled hodnot, naměřených za poslední čas aktivního pohybu.
            /// </summary>
            public List<T> ValueList { get; private set; }
            /// <summary>
            /// Výchozí bod pohybu.
            /// </summary>
            public T InitialValue { get; private set; }
            /// <summary>
            /// Minulý bod pohybu.
            /// </summary>
            public T LastValue { get; private set; }
            /// <summary>
            /// Aktuální bod pohybu. Hodnotu sem vepisuje delegát. V příštím volání je tato hodnota uložena v LastValue.
            /// </summary>
            public T CurrentValue { get; set; }
            /// <summary>
            /// Výchozí energie, zde už nelze měnit.
            /// </summary>
            public double InitialKineticEnergy { get; private set; }
            /// <summary>
            /// Zde se přenáší hodnota, určující změnu energie za jednotku času, jde tedy o vyjádření "koeficientu tření".
            /// Pohybová metoda ji může změnit, a příští výpočet již proběhne podle změněného koeficientu.
            /// Pokud bude nastavena hodnota 0, pak pohyb nemá setrvačnost (nikdy se nebude pohybovat setrvačností)
            /// Pokud bude nastavena hodnota 1, pak má pohyb absolutní setrvačnost (po rozpohybování se nikdy sám od sebe nezastaví).
            /// Setrvačnost vyjadřuje relativní změnu rychlosti pohybu za jednu sekundu.
            /// Příklad: při výchozí rychlosti 250 a EnergyMovementRatio = 0.80D bude po 1 sekundě rychlost = (0.80 * 250) = 200, 
            ///   ve druhé sekundě pak (0.80 * 200) = 160, ve třetí sekundě (0.80 * 160) = 128, atd.
            /// </summary>
            public double EnergyMovementRatio { get; set; }
            /// <summary>
            /// Aktuální poměrná energie (postupně se snižuje z hodnoty InitialKineticEnergy poměrem EnergyDeclineRatio)
            /// </summary>
            public double CurrentEnergyRatio { get; private set; }
            /// <summary>
            /// Aktuální hodnota, odvozená z výchozí energie (InitialKineticEnergy), uplynulého času a modifikovaná hodnotou EnergyDeclineRatio.
            /// Tato hodnota je integrálem času a rychlosti, představuje tedy vzdálenost od výchozího bodu.
            /// </summary>
            public double CurrentValueRatio { get; private set; }
            /// <summary>
            /// Sem může metoda nastavit true, pokud podle jejího názoru už pohyb skončil (je natolik malý, že je zanedbatelný).
            /// </summary>
            public bool Finished { get; set; }
        }
        private enum _MoveMode
        {
            None,
            Move,
            Fly
        }
        #endregion
        #region IDisposable Members
        void  IDisposable.Dispose()
        {
            this.Stop();
            this._Timer = null;
        }
        #endregion
    }
}
